package io.github.shenbinglife.jlock;

import io.github.shenbinglife.jlock.exception.LockCreateException;
import io.github.shenbinglife.jlock.exception.LockFailedException;
import io.github.shenbinglife.jlock.exception.UnLockFailedError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 基于文件的独占锁，不可重入
 *
 * @author shenbing
 * @date 2022/4/21
 */
public class JFileLock implements Lock {

    private static final Logger logger = LoggerFactory.getLogger(JFileLock.class);

    protected File file;

    public static JFileLock temporaryLock() {
        try {
            // prefix length should not < 3
            File jfilelock = File.createTempFile("jfilelock", ".lock");
            return new JFileLock(jfilelock);
        } catch (IOException e) {
            throw new LockCreateException("Can not create temporary file for lock", e);
        }
    }

    public JFileLock(File file) {
        if (file == null) {
            throw new NullPointerException("JFileLock can not accept null file");
        }

        this.file = file;
        String absolutePath = file.getAbsolutePath();
        if (this.file.exists() && file.isDirectory()) {
            throw new LockCreateException(
                    "Can not create lock file at directory exists: " + absolutePath);
        }
        logger.debug("Created JFileLock at path: {}", absolutePath);
    }

    public JFileLock(String file) {
        this(new File(file));
    }

    public JFileLock(Path path) {
        this(path.toFile());
    }

    @Override
    public void lock() {
        while (true) {
            if (tryLockOnce()) {
                break;
            }
            if (Thread.currentThread().isInterrupted()) {
                throw new LockFailedException("Thread interrupted when creating lock file");
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        while (true) {
            if (tryLockOnce()) {
                break;
            }
            Thread.sleep(200L);
        }
    }

    protected boolean tryLockOnce() {
        try {
            return file.createNewFile();
        } catch (IOException e) {
            throw new LockFailedException("IOException when creating lock file", e);
        }
    }

    @Override
    public boolean tryLock() {
        return tryLockOnce();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        long duration = TimeUnit.MILLISECONDS.convert(time, unit);
        while (true) {
            if (tryLockOnce()) {
                return true;
            }
            Thread.sleep(200L);
            duration -= 200L;
            if (duration <= 0) {
                return false;
            }
        }
    }

    @Override
    public void unlock() {
        if (!file.delete()) {
            throw new UnLockFailedError(
                    "Failed to delete lock file for unlock, file may not exists: " + file.getAbsolutePath());
        }
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException("unsupport create condition");
    }

    public boolean isLocked() {
        if (file.exists()) {
            if (file.isFile()) {
                return true;
            } else {
                throw new LockFailedException("Lock file is hold by directory");
            }
        }
        return false;
    }

    @Override
    public String toString() {
        return "JFileLock{" +
                "file=" + file +
                '}';
    }

    public static void main(String[] args) throws InterruptedException {
        JFileLock lock = new JFileLock("a.lock");
        System.out.println(lock.file.getAbsolutePath());

        lock.unlock();
        System.out.println("unlock");
        lock.lock();
        System.out.println("locked");

        lock.unlock();
        System.out.println("unlock");
    }
}
